package cn.donting.plugin.spring.boot.starter.plugin.autoconfiguration.web;

/**
 * @author donting
 * 2020-04-23 下午1:03
 */

import cn.donting.plugin.spring.boot.starter.Plugin;
import cn.donting.plugin.spring.boot.starter.properties.PluginProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter;
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Proxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.filter.FormContentFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.filter.RequestContextFilter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.i18n.FixedLocaleResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.resource.AppCacheManifestTransformer;
import org.springframework.web.servlet.resource.EncodedResourceResolver;
import org.springframework.web.servlet.resource.ResourceResolver;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.view.BeanNameViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.List;
import java.util.Map;

/**
 * spring mvc 配置
 *
 * @author donting
 * @see WebMvcAutoConfiguration
 * 2020-04-22 下午7:33
 */

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ConditionalOnWebApplication
@Slf4j
public class PluginMvcAutoConfiguration {

    private static final PluginServletContext plugServletContext = new PluginServletContext();

    public static final String DEFAULT_PREFIX = "";

    public static final String DEFAULT_SUFFIX = "";
    private static final String[] SERVLET_LOCATIONS = {"/"};

    static String[] getResourceLocations(String[] staticLocations) {
        String[] locations = new String[staticLocations.length + SERVLET_LOCATIONS.length];
        System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
        System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length, SERVLET_LOCATIONS.length);
        return locations;
    }

    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }

    @Bean
    @ConditionalOnMissingBean(FormContentFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
    public OrderedFormContentFilter formContentFilter() {
        return new OrderedFormContentFilter();
    }

    /**
     * 实现 WebMvcConfigurer 解决 resourceHandlerMapping 的问题
     * 不继承WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter ：： WebMvcAutoConfigurationAdapter 的实现使用的私有
     */
    @Configuration
    @Import({PluginMvcAutoConfiguration.PlugEnableWebMvcConfiguration.class})
    @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
    @Order(0)
    public static class PlugMvcAutoConfigurationAdapter implements WebMvcConfigurer {
        private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);

        private final ResourceProperties resourceProperties;

        private final WebMvcProperties mvcProperties;

        private final ListableBeanFactory beanFactory;

        private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;

        final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;

        public PlugMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
                                               ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
                                               ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider) {
            this.resourceProperties = resourceProperties;
            this.mvcProperties = mvcProperties;
            this.beanFactory = beanFactory;
            this.messageConvertersProvider = messageConvertersProvider;
            this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
        }

        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            this.messageConvertersProvider
                    .ifAvailable((customConverters) ->
                            converters.addAll(customConverters.getConverters()));

        }

        @Override
        public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
            if (this.beanFactory.containsBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) {
                Object taskExecutor = this.beanFactory
                        .getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
                if (taskExecutor instanceof AsyncTaskExecutor) {
                    configurer.setTaskExecutor(((AsyncTaskExecutor) taskExecutor));
                }
            }
            Duration timeout = this.mvcProperties.getAsync().getRequestTimeout();
            if (timeout != null) {
                configurer.setDefaultTimeout(timeout.toMillis());
            }
        }

        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
            configurer.setUseRegisteredSuffixPatternMatch(
                    this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
        }

        @Override
        public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
            WebMvcProperties.Contentnegotiation contentnegotiation = this.mvcProperties.getContentnegotiation();
            configurer.favorPathExtension(contentnegotiation.isFavorPathExtension());
            configurer.favorParameter(contentnegotiation.isFavorParameter());
            if (contentnegotiation.getParameterName() != null) {
                configurer.parameterName(contentnegotiation.getParameterName());
            }
            Map<String, MediaType> mediaTypes = this.mvcProperties.getContentnegotiation().getMediaTypes();
            mediaTypes.forEach(configurer::mediaType);
        }

        @Bean
        @ConditionalOnMissingBean
        public InternalResourceViewResolver defaultViewResolver() {
            InternalResourceViewResolver resolver = new InternalResourceViewResolver();
            resolver.setPrefix(this.mvcProperties.getView().getPrefix());
            resolver.setSuffix(this.mvcProperties.getView().getSuffix());
            return resolver;
        }

        @Bean
//        @ConditionalOnBean(View.class)
        @ConditionalOnMissingBean
        public BeanNameViewResolver beanNameViewResolver() {
            BeanNameViewResolver resolver = new BeanNameViewResolver();
            resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
            return resolver;
        }
//
//        @Bean
//        @ConditionalOnBean(ViewResolver.class)
//        @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
//        public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
//            ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
//            resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
////            //手动设置viewResolver
////            Collection<ViewResolver> matchingBeans =
////                    BeanFactoryUtils.beansOfTypeIncludingAncestors((ListableBeanFactory) beanFactory, ViewResolver.class).values();
////            resolver.setViewResolvers(new ArrayList<>(matchingBeans));
//            // ContentNegotiatingViewResolver uses all the other view resolvers to locate
//            // a view so it should have a high precedence
//            resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
//            return resolver;
//        }

        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
        public LocaleResolver localeResolver() {
            if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
                return new FixedLocaleResolver(this.mvcProperties.getLocale());
            }
            AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
            localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
            return localeResolver;
        }

        @Override
        public MessageCodesResolver getMessageCodesResolver() {
            if (this.mvcProperties.getMessageCodesResolverFormat() != null) {
                DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver();
                resolver.setMessageCodeFormatter(this.mvcProperties.getMessageCodesResolverFormat());
                return resolver;
            }
            return null;
        }

        @Override
        public void addFormatters(FormatterRegistry registry) {
            ApplicationConversionService.addBeans(registry, this.beanFactory);
        }

        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {

            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
                return;
            }
            Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
            CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
            if (!registry.hasMappingForPattern("/webjars/**")) {
                customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                        .addResourceLocations("classpath:/META-INF/resources/webjars/")
                        .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
            }
            String staticPathPattern = this.mvcProperties.getStaticPathPattern();
            if (!registry.hasMappingForPattern(staticPathPattern)) {
                customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                        .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                        .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
            }
        }

        private Integer getSeconds(Duration cachePeriod) {
            return (cachePeriod != null) ? (int) cachePeriod.getSeconds() : null;
        }

        private void customizeResourceHandlerRegistration(ResourceHandlerRegistration registration) {
            if (this.resourceHandlerRegistrationCustomizer != null) {
                this.resourceHandlerRegistrationCustomizer.customize(registration);
            }
        }

        @Bean
        @ConditionalOnMissingBean({RequestContextListener.class, RequestContextFilter.class})
        @ConditionalOnMissingFilterBean(RequestContextFilter.class)
        public static RequestContextFilter requestContextFilter() {
            return new OrderedRequestContextFilter();
        }


    }

    @Configuration
    @ConditionalOnEnabledResourceChain
    static class ResourceChainCustomizerConfiguration {
        @Bean
        ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() {
            return new ResourceChainResourceHandlerRegistrationCustomizer();
        }
    }

    /***
     * 继承了EnableWebMvcConfiguration
     */
    @Configuration(proxyBeanMethods = false)
    public static class PlugEnableWebMvcConfiguration extends WebMvcAutoConfiguration.EnableWebMvcConfiguration {
        private ListableBeanFactory beanFactory;

        public PlugEnableWebMvcConfiguration(ResourceProperties resourceProperties, ObjectProvider<WebMvcProperties> mvcPropertiesProvider,
                                             ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider, ListableBeanFactory beanFactory) {
            super(resourceProperties, mvcPropertiesProvider, mvcRegistrationsProvider, beanFactory);
            this.beanFactory = beanFactory;
            setServletContext(plugServletContext);
        }

        private void webMvcConfigurationSupportConfig(String methodName,Class parameterTypes ,Object args) {
            Map<String, WebMvcConfigurationSupport> beansOfType = beanFactory.getBeansOfType(WebMvcConfigurationSupport.class);
            for (Map.Entry<String, WebMvcConfigurationSupport> stringWebMvcConfigurationSupportEntry : beansOfType.entrySet()) {
                if (!stringWebMvcConfigurationSupportEntry.getKey().equals("pluginMvcAutoConfiguration.PlugEnableWebMvcConfiguration")) {
                    WebMvcConfigurationSupport value = stringWebMvcConfigurationSupportEntry.getValue();
                    Class<? extends WebMvcConfigurationSupport> aClass = value.getClass();

                    if (aClass.getName().indexOf("CGLIB$$") > -1 || java.lang.reflect.Proxy.isProxyClass(aClass)) {
                        //代理
                        try {
                            Method addInterceptors = value.getClass().getSuperclass().getDeclaredMethod(methodName, parameterTypes);
                            addInterceptors.setAccessible(true);
                            addInterceptors.invoke(value, args);
                        } catch (NoSuchMethodException e) {
                            log.debug(e.getMessage());

                        } catch (IllegalAccessException e) {
                            log.debug(e.getMessage());
                        } catch (InvocationTargetException e) {
                            log.debug(e.getMessage());
                        }
                    } else {
                        try {
                            Method addInterceptors = value.getClass().getDeclaredMethod(methodName, parameterTypes);
                            addInterceptors.setAccessible(true);
                            addInterceptors.invoke(value, args);
                        } catch (NoSuchMethodException e) {
                            log.debug(e.getMessage());

                        } catch (IllegalAccessException e) {
                            log.debug(e.getMessage());
                        } catch (InvocationTargetException e) {
                            log.debug(e.getMessage());
                        }
                    }
                }
            }
        }

        @Override
        protected void addFormatters(FormatterRegistry registry) {
            webMvcConfigurationSupportConfig("addFormatters", FormatterRegistry.class,registry);
            super.addFormatters(registry);
        }

        @Override
        protected void addInterceptors(InterceptorRegistry registry) {
            webMvcConfigurationSupportConfig("addInterceptors", InterceptorRegistry.class,registry);
            super.addInterceptors(registry);
        }

        @Override
        protected void addResourceHandlers(ResourceHandlerRegistry registry) {
            webMvcConfigurationSupportConfig("addResourceHandlers",ResourceHandlerRegistry.class, registry);
            super.addResourceHandlers(registry);
        }

        @Override
        protected void addCorsMappings(CorsRegistry registry) {
            webMvcConfigurationSupportConfig("addCorsMappings",CorsRegistry.class, registry);
            super.addCorsMappings(registry);
        }

        @Override
        protected void addViewControllers(ViewControllerRegistry registry) {
            webMvcConfigurationSupportConfig("addViewControllers",ViewControllerRegistry.class, registry);
            super.addViewControllers(registry);
        }


        @Override
        protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
            webMvcConfigurationSupportConfig("addArgumentResolvers",List.class, argumentResolvers);
            super.addArgumentResolvers(argumentResolvers);
        }

        @Override
        protected void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
            webMvcConfigurationSupportConfig("addReturnValueHandlers",List.class, returnValueHandlers);
            super.addReturnValueHandlers(returnValueHandlers);
        }

        @Override
        protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            webMvcConfigurationSupportConfig("configureMessageConverters",List.class, converters);
            super.configureMessageConverters(converters);
        }

        @Override
        protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
            webMvcConfigurationSupportConfig("extendMessageConverters",List.class, converters);
            super.extendMessageConverters(converters);
        }

        @Override
        protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
            webMvcConfigurationSupportConfig("extendMessageConverters",List.class, exceptionResolvers);
            super.configureHandlerExceptionResolvers(exceptionResolvers);
        }

        @Override
        protected void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
            webMvcConfigurationSupportConfig("extendHandlerExceptionResolvers",List.class, exceptionResolvers);
            super.extendHandlerExceptionResolvers(exceptionResolvers);
        }

        @Override
        protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
            //在这里自定义 RequestMappingHandlerMapping 以往 RequestMapping 添加appId 前缀
            String[] split = PluginProperties.visits();
            if (split.length < 2) {
                return new RequestMappingHandlerMapping();
            }
            for (String s : split) {
                if (s.toUpperCase().equals("URL")) {
                    Object plugMain = beanFactory.getBean("pluginMain");
                    Plugin annotation = plugMain.getClass().getAnnotation(Plugin.class);
                    if (annotation == null) {
                        throw new RuntimeException(" load PlugEnableWebMvcConfiguration Plugin is null");
                    }
                    return new PluginRequestMappingHandlerMapping(annotation.id());
                }
            }
            return new RequestMappingHandlerMapping();

        }
    }

    interface ResourceHandlerRegistrationCustomizer {

        void customize(ResourceHandlerRegistration registration);

    }

    static class ResourceChainResourceHandlerRegistrationCustomizer implements ResourceHandlerRegistrationCustomizer {

        @Autowired
        private ResourceProperties resourceProperties = new ResourceProperties();

        @Override
        public void customize(ResourceHandlerRegistration registration) {
            ResourceProperties.Chain properties = this.resourceProperties.getChain();
            configureResourceChain(properties, registration.resourceChain(properties.isCache()));
        }

        private void configureResourceChain(ResourceProperties.Chain properties, ResourceChainRegistration chain) {
            ResourceProperties.Strategy strategy = properties.getStrategy();
            if (properties.isCompressed()) {
                chain.addResolver(new EncodedResourceResolver());
            }
            if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) {
                chain.addResolver(getVersionResourceResolver(strategy));
            }
            if (properties.isHtmlApplicationCache()) {
                chain.addTransformer(new AppCacheManifestTransformer());
            }
        }

        private ResourceResolver getVersionResourceResolver(ResourceProperties.Strategy properties) {
            VersionResourceResolver resolver = new VersionResourceResolver();
            if (properties.getFixed().isEnabled()) {
                String version = properties.getFixed().getVersion();
                String[] paths = properties.getFixed().getPaths();
                resolver.addFixedVersionStrategy(version, paths);
            }
            if (properties.getContent().isEnabled()) {
                String[] paths = properties.getContent().getPaths();
                resolver.addContentVersionStrategy(paths);
            }
            return resolver;
        }

    }


}
